library stripe_terminal;

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'dart:convert';

part "models/log.dart";
part "models/reader.dart";
part "models/reader_display.dart";
part "models/payment_intent.dart";
part 'models/payment_method.dart';
part "models/discover_config.dart";
part "models/collect_configuration.dart";

enum CustomPaymentStatus {
  noPayment,
  startRerrievePaymentIntent,
  RertievePaymentIntentFail,
  startCollectPaymentMethod,
  collectPaymentMethodFail,
  startProcessPayment,
  processPaymentFail,
  processPaymentSuccess,
  startCreatePaymentIntent,
  createPaymentIntentFail,
  createPaymentIntentSuccess,
}

class StripeTerminal {
  // Method Channel on which the package operates with the native platform.
  static const MethodChannel _channel = MethodChannel('stripe_terminal');
  final Future<String> Function() _fetchToken;
  /// Creates an internal `StripeTerminal` instance
  StripeTerminal._internal({
    /// A callback function that is supposed to return a
    /// terminal connection token from backend.
    required Future<String> Function() fetchToken,
    void Function(StripeReader reader)? onUnexpectedReaderDisconnect,
    void Function(StripeReader reader)? onReaderReconnectFailed,
    void Function()? onReaderReconnectSucceeded,
    void Function()? onReaderReconnectStarted,
    void Function(ConnectionStatus connectionStatus)? onConnectionStatusChange,
    void Function(PaymentStatus paymentStatus)? onPaymentStatusChange,
    void Function(CustomPaymentStatus paymentStatus, Map<String, dynamic>? exception, StripePaymentIntent? paymentIntent)? callbackPaymentIntentStatus,
    void Function(int messageType)? onRequestReaderDisplayMessage,
    void Function(String errorMessage)? onInitError,
    void Function(Map<String, dynamic>? param)? onReportAvailableUpdate,
    void Function(String version)? onStartInstallingUpdate,
    void Function(double progress)? onReportReaderSoftwareUpdateProgress,
    void Function(Map<String, dynamic>? param)? onFinishInstallingUpdate,
  }) : _fetchToken = fetchToken {
    _channel.setMethodCallHandler((call) async {
      switch (call.method) {
        case "requestConnectionToken":
          return _fetchToken();
        case "onNativeLog":
          _logsStreamController.add(StripeLog(
            code: call.arguments["code"] as String,
            message: call.arguments["message"] as String,
          ));
          break;
        case "onReadersFound":
          List readers = call.arguments;
          _readerStreamController.add(
            readers.map<StripeReader>((e) => StripeReader.fromJson(e)).toList(),
          );
          return _fetchToken();
        case "onUnexpectedReaderDisconnect":
          if (onUnexpectedReaderDisconnect != null) {
            onUnexpectedReaderDisconnect!(StripeReader.fromJson(call.arguments));
          }
          break;
        case "onReaderReconnectFailed":
          if (onReaderReconnectFailed != null) {
            onReaderReconnectFailed!(StripeReader.fromJson(call.arguments));
          }
          break;
        case "onReaderReconnectSucceeded":
          if (onReaderReconnectSucceeded != null) {
            onReaderReconnectSucceeded!();
          }
          break;
        case "onReaderReconnectStarted":
          if (onReaderReconnectStarted != null) {
            onReaderReconnectStarted!();
          }
          break;
        case "onConnectionStatusChange":
          if (onConnectionStatusChange != null) {
            var status = ConnectionStatus.notConnected;
            int? statusId = call.arguments as int?;
            if (statusId != null) {
              status = ConnectionStatus.values[statusId];
            }
            onConnectionStatusChange!(status);
          }
          break;
        case "onPaymentStatusChange":
          if (onPaymentStatusChange != null) {
            var status = PaymentStatus.notReady;
            int? statusId = call.arguments as int?;
            if (statusId != null) {
              status = PaymentStatus.values[statusId];
            }
            onPaymentStatusChange!(status);
          }
          break;
        case "callbackPaymentIntentStatus":
          CustomPaymentStatus paymentStatus = CustomPaymentStatus.noPayment;
          try {
            paymentStatus = getPaymentStatus((call.arguments["paymentStatus"] as int) ?? 0);
          } catch(e) {
            print('CallbackPaymentIntentStatus Error when paymentStatus:${e.toString()}');
          }
          Map<String, dynamic>? exception;
          try {
            String? map = call.arguments["exception"].toString();
            exception = jsonDecode(map);
          } catch(e) {
            print('CallbackPaymentIntentStatus Error when exception:${e.toString()}');
          }
          StripePaymentIntent? paymentIntent;
          try {
            paymentIntent = StripePaymentIntent.fromMap(call.arguments["paymentIntent"]);
          } catch(e) {
            print('CallbackPaymentIntentStatus Error when paymentIntent:${e.toString()}');
          }
          if (callbackPaymentIntentStatus != null) {
            callbackPaymentIntentStatus(paymentStatus, exception, paymentIntent);
          }
          break;
        case "onRequestReaderDisplayMessage":
          int messageType = call.arguments;
          if (onRequestReaderDisplayMessage != null) {
            onRequestReaderDisplayMessage(messageType);
          }
          break;

        case "callbackStripeInitError":
          String errorMessage = call.arguments;
          if (onInitError != null) {
            onInitError(errorMessage);
          }
          break;
        case "onReportAvailableUpdate":
          try {
            if (onReportAvailableUpdate != null) {
              onReportAvailableUpdate(jsonDecode(jsonEncode(call.arguments)));
            }
          } catch(e) {
            print('onReportAvailableUpdate Error when exception:${e.toString()}');
          }
          break;
        case "onStartInstallingUpdate":
          String version = call.arguments;
          if (onStartInstallingUpdate != null) {
            onStartInstallingUpdate(version);
          }
          break;
        case "onReportReaderSoftwareUpdateProgress":
          double pregress = call.arguments;
          if (onReportReaderSoftwareUpdateProgress != null) {
            onReportReaderSoftwareUpdateProgress(pregress);
          }
          break;
        case "onFinishInstallingUpdate":
          if (onFinishInstallingUpdate != null) {
            onFinishInstallingUpdate(jsonDecode(jsonEncode(call.arguments)));
          }
          break;
        default:
          return null;
      }
    });
  }

  /// Initializes the terminal SDK
  static Future<StripeTerminal> getInstance({
    /// A callback function that returns a Future which resolves to a connection token from your backend
    /// Check out more at https://stripe.com/docs/terminal/payments/setup-integration#connection-token
    required Future<String> Function() fetchToken,
    void Function(StripeReader reader)? onUnexpectedReaderDisconnect,
    void Function(StripeReader reader)? onReaderReconnectFailed,
    void Function()? onReaderReconnectSucceeded,
    void Function()? onReaderReconnectStarted,
    void Function(ConnectionStatus connectionStatus)? onConnectionStatusChange,
    void Function(PaymentStatus paymentStatus)? onPaymentStatusChange,
    void Function(CustomPaymentStatus paymentStatus, Map<String, dynamic>? exception, StripePaymentIntent? paymentIntent)? callbackPaymentIntentStatus,
    void Function(int messageType)? onRequestReaderDisplayMessage,
    void Function(String errorMessage)? onInitError,
    void Function(Map<String, dynamic>? exception)? onReportAvailableUpdate,
    void Function(String version)? onStartInstallingUpdate,
    void Function(double progress)? onReportReaderSoftwareUpdateProgress,
    void Function(Map<String, dynamic>? param)? onFinishInstallingUpdate,
  }) async {
    StripeTerminal _stripeTerminal = StripeTerminal._internal(
        fetchToken: fetchToken,
        onUnexpectedReaderDisconnect: onUnexpectedReaderDisconnect,
        onReaderReconnectStarted: onReaderReconnectStarted,
        onReaderReconnectSucceeded: onReaderReconnectSucceeded,
        onReaderReconnectFailed: onReaderReconnectFailed,
        onConnectionStatusChange: onConnectionStatusChange,
        onPaymentStatusChange: onPaymentStatusChange,
        callbackPaymentIntentStatus: callbackPaymentIntentStatus,
        onRequestReaderDisplayMessage: onRequestReaderDisplayMessage,
        onInitError: onInitError,
        onReportAvailableUpdate: onReportAvailableUpdate,
        onStartInstallingUpdate: onStartInstallingUpdate,
        onReportReaderSoftwareUpdateProgress: onReportReaderSoftwareUpdateProgress,
        onFinishInstallingUpdate: onFinishInstallingUpdate,
    );
    await _channel.invokeMethod("init");
    return _stripeTerminal;
  }

  /// Stream controller for the logs coming from the native platform
  final StreamController<StripeLog> _logsStreamController =
      StreamController<StripeLog>();

  /// Gives you the native logs of this plugin. If some features are not working for you,
  /// you can listen to the native logs to understand whats going wrong.
  Stream<StripeLog> get onNativeLogs => _logsStreamController.stream;

  /// Connects to a bluetooth reader, only works if you have scanned devices within this session.
  ///
  /// Always run `discoverReaders` before calling this function
  @Deprecated(
    "Please use `connectBluetoothReader` function instead to connect to the bluetooth reader",
  )
  Future<bool> connectToReader(
    /// Serial number of the reader to connect with
    String readerSerialNumber, {

    /// The id of the location on which you want to conenct this bluetooth reader with.
    ///
    /// Either you have to provide a location here or the device should already be registered to a location
    String? locationId,

    /// Set autoReconnect on unexpectedDisconnect, default is true.
    bool autoReconnect = true,

  }) async {
    bool? connected =
        await _channel.invokeMethod<bool?>("connectBluetoothReader", {
      "locationId": locationId,
      "readerSerialNumber": readerSerialNumber,
      "autoReconnectOnUnexpectedDisconnect": autoReconnect
    });
    if (connected == null) {
      throw Exception("Unable to connect to the reader");
    } else {
      return connected;
    }
  }

  /// Connects to a bluetooth reader, only works if you have scanned devices within this session.
  ///
  /// Always run `discoverReaders` before calling this function
  Future<bool> connectBluetoothReader(
    /// Serial number of the bluetooth reader to connect with
    String readerSerialNumber, {

    /// The id of the location on which you want to conenct this bluetooth reader with.
    ///
    /// Either you have to provide a location here or the device should already be registered to a location

    String? locationId,

    /// Set autoReconnect on unexpectedDisconnect, default is true.
    bool autoReconnect = true,
  }) async {
    bool? connected =
        await _channel.invokeMethod<bool?>("connectBluetoothReader", {
      "locationId": locationId,
      "readerSerialNumber": readerSerialNumber,
      "autoReconnectOnUnexpectedDisconnect": autoReconnect
    });
    if (connected == null) {
      throw Exception("Unable to connect to the reader");
    } else {
      return connected;
    }
  }

  /// Connects to a internet reader, only works if you have scanned devices within this session.
  ///
  /// Always run `discoverReaders` before calling this function
  Future<bool> connectToInternetReader(
    /// Serial number of the internet reader to connect with
    String readerSerialNumber, {

    /// Weather the connection process should fail if the device is already in use
    bool failIfInUse = false,

    /// Set autoReconnect on unexpectedDisconnect, default is true.
    bool autoReconnect = true,
  }) async {
    bool? connected =
        await _channel.invokeMethod<bool?>("connectToInternetReader", {
      "failIfInUse": failIfInUse,
      "readerSerialNumber": readerSerialNumber,
      "autoReconnectOnUnexpectedDisconnect": autoReconnect
    });
    if (connected == null) {
      throw Exception("Unable to connect to the reader");
    } else {
      return connected;
    }
  }

  /// Connects to a usb reader, only works if you have scanned devices within this session.
  ///
  /// Always run `discoverReaders` before calling this function
  Future<bool> connectUSBReader(
      /// Serial number of the bluetooth reader to connect with
      String readerSerialNumber, {

        /// The id of the location on which you want to conenct this bluetooth reader with.
        ///
        /// Either you have to provide a location here or the device should already be registered to a location
        String? locationId,

        /// Set autoReconnect on unexpectedDisconnect, default is true.
        bool autoReconnect = true,
      }) async {
    bool? connected =
    await _channel.invokeMethod<bool?>("connectUSBReader", {
      "locationId": locationId,
      "readerSerialNumber": readerSerialNumber,
      "autoReconnectOnUnexpectedDisconnect": autoReconnect
    });
    if (connected == null) {
      throw Exception("Unable to connect to the reader");
    } else {
      return connected;
    }
  }

  /// Disconnects from a reader, only works if you are connected to a device
  ///
  /// Always run `connectToReader` before calling this function
  Future<bool> disconnectFromReader() async {
    bool? disconnected =
        await _channel.invokeMethod<bool?>("disconnectFromReader");
    if (disconnected == null) {
      throw Exception("Unable to disconnect from the reader");
    } else {
      return disconnected;
    }
  }

  /// cancel reconnect from a reader, only works if you are reconnecting to a device
  Future<bool> cancelReconnectFromReader() async {
    bool? cancelReconnect =
    await _channel.invokeMethod<bool?>("reconnect#stop");
    if (cancelReconnect == null) {
      throw Exception("Unable to cancel reconnect from the reader");
    } else {
      return cancelReconnect;
    }
  }

  /// Displays the content to the connected reader's display
  Future<void> setReaderDisplay(
    /// Display information for the reader to be shown on the screen
    ///
    /// Supports on the devices which has a display
    ReaderDisplay readerDisplay,
  ) async {
    await _channel.invokeMethod<bool?>("setReaderDisplay", {
      "readerDisplay": readerDisplay.toMap(),
    });
  }

  /// Clears connected reader's displays
  Future<void> clearReaderDisplay() async {
    await _channel.invokeMethod<bool?>("clearReaderDisplay");
  }

  /// Checks the connection status of the SDK
  Future<ConnectionStatus> connectionStatus() async {
    int? statusId = await _channel.invokeMethod<int>("connectionStatus");
    if (statusId == null) {
      throw Exception("Unable to get connection status");
    } else {
      return ConnectionStatus.values[statusId];
    }
  }

  /// Fetches the connected reader from the SDK. `null` if not connected
  Future<StripeReader?> fetchConnectedReader() async {
    Map? reader = await _channel.invokeMethod<Map>("fetchConnectedReader");
    if (reader == null) {
      return null;
    } else {
      return StripeReader.fromJson(reader);
    }
  }

  // @Deprecated('V3.0.0 起已废弃')
  // /// Extracts payment method from the reader
  // ///
  // /// Only support `insert` operation on the reader
  // Future<StripePaymentMethod> readReusableCardDetail() async {
  //   Map cardDetail = await _channel.invokeMethod("readReusableCardDetail");
  //   return StripePaymentMethod.fromJson(cardDetail);
  // }

  late StreamController<List<StripeReader>> _readerStreamController;

  /// Starts scanning readers in the vicinity. This will return a list of readers.
  ///
  /// Can contain an empty array if no readers are found.
  ///
  /// [simulated] se to `true` will simulate readers which can be connected and tested.
  Stream<List<StripeReader>> discoverReaders(
    /// Configuration for the discovry process
    DiscoverConfig config,
  ) {
    _readerStreamController = StreamController<List<StripeReader>>();

    _channel.invokeMethod("discoverReaders#start", {
      "config": config.toMap(),
    });
    _readerStreamController.onCancel = () {
      _channel.invokeMethod("discoverReaders#stop");
      _readerStreamController.close();
    };
    return _readerStreamController.stream;
  }

  /// Starts reading payment method based on payment intent.
  ///
  /// Payment intent is supposed to be generated on your backend and the `clientSecret` of the payment intent
  /// should be passed to this function.
  ///
  /// Once passed, the payment intent will be fetched and the payment method is captured. A sucessful function call
  /// should return an instance of `StripePaymentIntent` with status `requiresPaymentMethod`;
  ///
  /// Only supports `swipe`, `tap` and `insert` method
  Future<StripePaymentIntent> collectPaymentMethod(
      // Client secret of the payment intent which you want to collect payment mwthod for
      String clientSecret, {

        /// Configruation for the collection process
        CollectConfiguration? collectConfiguration = const CollectConfiguration(
          skipTipping: true,
        ),
      }) async {
    Map paymentIntent = await _channel.invokeMethod("collectPaymentMethod", {
      "paymentIntentClientSecret": clientSecret,
      "collectConfiguration": collectConfiguration?.toMap(),
    });

    return StripePaymentIntent.fromMap(paymentIntent);
  }

  /// 创建支付
  Future<StripePaymentIntent> createPaymentMethod(
      int amount,
      String orderId,
      String orderSn,
      String storeName,
      {
        /// Configruation for the collection process
        CollectConfiguration? collectConfiguration = const CollectConfiguration(
          skipTipping: true,
        ),
        autoCapture = true,
      }) async {
    Map paymentIntent = await _channel.invokeMethod("createPaymentMethod", {
      "paymentAmount": amount,
      "orderId": orderId,
      "orderSn": orderSn,
      "collectConfiguration": collectConfiguration?.toMap(),
      "storeName": storeName,
      "autoCapture": autoCapture
    });

    return StripePaymentIntent.fromMap(paymentIntent);
  }

  /// 采集支付
  Future<StripePaymentIntent> collectAfterCreatePaymentMethod(
      String orderId,
      {
        /// Configruation for the collection process
        CollectConfiguration? collectConfiguration = const CollectConfiguration(
          skipTipping: true,
        ),
      }) async {
    Map paymentIntent = await _channel.invokeMethod("collectAfterCreatePayment", {
      "collectConfiguration": collectConfiguration?.toMap(),
      "orderId": orderId
    });

    return StripePaymentIntent.fromMap(paymentIntent);
  }

  /// 创建支付方式上报成功后开始采集支付方式

  Future<bool> cancelPayment() async {
    bool cancelResult = await _channel.invokeMethod("cancelPayment");

    return cancelResult;
  }

  Future startUpdate() async {
    try {
      bool? canUpdate = await _channel.invokeMethod('updateCurrentReader');
    } catch(e) {
      if (e.runtimeType == PlatformException) {
        PlatformException exception = e as PlatformException;
        throw PlatformException(code: e.code, message: e.message);
      }
    }
  }
}


CustomPaymentStatus getPaymentStatus(int index) {
  Map<int, CustomPaymentStatus> status = {
    0 : CustomPaymentStatus.noPayment,
    1 : CustomPaymentStatus.startRerrievePaymentIntent,
    2 : CustomPaymentStatus.RertievePaymentIntentFail,
    3 : CustomPaymentStatus.startCollectPaymentMethod,
    4 : CustomPaymentStatus.collectPaymentMethodFail,
    5 : CustomPaymentStatus.startProcessPayment,
    6 : CustomPaymentStatus.processPaymentFail,
    7 : CustomPaymentStatus.processPaymentSuccess,
    8 : CustomPaymentStatus.startCreatePaymentIntent,
    9 : CustomPaymentStatus.createPaymentIntentFail,
    10: CustomPaymentStatus.createPaymentIntentSuccess,
  };

  return status[index] ?? CustomPaymentStatus.noPayment;
}

int getPaymentStatusCode (CustomPaymentStatus status) {
  switch (status) {
    case CustomPaymentStatus.noPayment:
      return 0;
    case CustomPaymentStatus.startRerrievePaymentIntent:
      return 1;
    case CustomPaymentStatus.RertievePaymentIntentFail:
      return 2;
    case CustomPaymentStatus.startCollectPaymentMethod:
      return 3;
    case CustomPaymentStatus.collectPaymentMethodFail:
      return 4;
    case CustomPaymentStatus.startProcessPayment:
      return 5;
    case CustomPaymentStatus.processPaymentFail:
      return 6;
    case CustomPaymentStatus.processPaymentSuccess:
      return 7;
    case CustomPaymentStatus.startCreatePaymentIntent:
      return 8;
    case CustomPaymentStatus.createPaymentIntentFail:
      return 9;
    case CustomPaymentStatus.createPaymentIntentSuccess:
      return 10;
  }
}
